package orm

import (
	t "gitee.com/KotlinToGo/type"
	"math"
	"reflect"
	"strings"
)

// Scan : (原Select)
//	参数<pointer>为空时，选择查询一条或多条数据并绑定到传入对象上。
//  参数<pointer>非空时，查询一条或多条数据并绑定到 pointer上。
//	当绑定对象传入的是string类型时, 返回多条结果集, 需要使用 Find() 来获取最终结果。
func (p *Orm) Scan(pointer ...interface{}) error {
	switch p.GetIBinder().GetBindType() {
	case ObjectStruct, ObjectMap, ObjectMapT:
		p.Limit(1)
	}
	// 构建sql
	sqlStr, args, err := p.Sql()
	if err != nil {
		return err
	}
	// 执行查询
	_, err = p.GetISession().Query(sqlStr, args...)
	if len(pointer) != 0 {
		p.Table(&pointer)
	}
	if len(pointer) > 1 {
		return err
	}
	return err
}

// First : select one row , relation limit set
func (p *Orm) First(pointer ...interface{}) (result Data, err error) {
	p.GetIBinder().SetBindType(ObjectString)
	err = p.Limit(1).Scan(pointer)
	if err != nil {
		return
	}
	res := p.GetISession().GetBindAll()
	if len(res) > 0 {
		result = res[0]
	}
	return
}

// Find : select more rows , relation limit set
func (p *Orm) Find(pointer ...interface{}) (result []Data, err error) {
	p.GetIBinder().SetBindType(ObjectString)
	tabname := p.GetISession().GetIBinder().GetBindName()
	prefix := p.GetISession().GetIBinder().GetBindPrefix()
	tabname2 := strings.TrimPrefix(tabname, prefix)
	p.ResetTable()
	p.Table(tabname2)
	err = p.Scan(pointer)
	result = p.GetISession().GetBindAll()
	return
}

// Count : select count rows
func (p *Orm) Count(args ...string) (int64, error) {
	fields := "*"
	if len(args) > 0 {
		fields = args[0]
	}
	count, err := p.unionBuild("count", fields)
	if count == nil {
		return 0, err
	}
	return t.New(count).Int64(), err
}

// Sum : select sum field
func (p *Orm) Sum(sum string) (interface{}, error) {
	return p.unionBuild("sum", sum)
}

// Avg : select avg field
func (p *Orm) Avg(avg string) (interface{}, error) {
	return p.unionBuild("avg", avg)
}

// Max : select max field
func (p *Orm) Max(max string) (interface{}, error) {
	return p.unionBuild("max", max)
}

// Min : select min field
func (p *Orm) Min(min string) (interface{}, error) {
	return p.unionBuild("min", min)
}

// Pluck 获取一列数据, 第二个字段可以指定另一个字段的值作为这一列数据的key
func (p *Orm) Pluck(pointer interface{},field string, fieldKey ...string) (v interface{}, err error) {
	var resMap = make(map[interface{}]interface{}, 0)
	var resSlice = make([]interface{}, 0)

	res, err := p.Find(pointer)

	if err != nil {
		return
	}

	if len(res) > 0 {
		for _, val := range res {
			if len(fieldKey) > 0 {
				resMap[val[fieldKey[0]]] = val[field]
			} else {
				resSlice = append(resSlice, val[field])
			}
		}
	}
	if len(fieldKey) > 0 {
		v = resMap
	} else {
		v = resSlice
	}
	return
}



// Value is get a row of a field value
func (p *Orm) Value(field string) (v interface{}, err error) {
	res, err := p.First()
	if v, ok := res[field]; ok {
		return v, err
	}
	return
}



// Chunk : 分块处理数据,当要处理很多数据的时候, 我不需要知道具体是多少数据, 我只需要每次取limit条数据,
// 然后不断的增加offset去取更多数据, 从而达到分块处理更多数据的目的
//TODO 后续增加 gorotine 支持, 提高批量数据处理效率, 预计需要增加获取更多链接的支持
func (p *Orm) Chunk(limit int, callback func([]Data) error,pointer ...interface{}) (err error) {
	var page = 1
	var tabname = p.GetISession().GetIBinder().GetBindName()
	prefix := p.GetISession().GetIBinder().GetBindPrefix()
	tabname2 := strings.TrimPrefix(tabname, prefix)
	// 先执行一条看看是否报错, 同时设置指定的limit, offset
	result, err := p.Table(tabname2).Limit(limit).Page(page).Find(pointer)
	if err != nil {
		return
	}
	for len(result) > 0 {
		if err = callback(result); err != nil {
			break
		}
		page++
		// 清理绑定数据, 进行下一次操作, 因为绑定数据是每一次执行的时候都会解析并保存的
		// 而第二次以后执行的, 都会再次解析并保存, 数据结构是slice, 故会累积起来
		p.ClearBindValues()
		result, _ = p.Page(page).Find()
	}
	return
}

// ChunkStruct : 同Chunk,只不过不用返回map, 而是绑定数据到传入的对象上
// 这里一定要传入绑定struct
func (p *Orm) ChunkStruct(limit int, callback func() error,pointer ...interface{}) (err error) {
	var page = 0
	//var tableName = dba.GetISession().GetIBinder().GetBindName()
	// 先执行一条看看是否报错, 同时设置指定的limit, offset
	err = p.Limit(limit).Offset(page * limit).Scan()
	if err != nil {
		return
	}
	switch p.GetIBinder().GetBindType() {
	case ObjectStruct, ObjectMap, ObjectMapT:
		var ibinder = p.GetIBinder()
		var result = ibinder.GetBindResult()
		for result != nil {
			if err = callback(); err != nil {
				break
			}
			page++
			// 清空结果
			//result = nil
			var rfRes = reflect.ValueOf(result)
			rfRes.Set(reflect.Zero(rfRes.Type()))
			// 清理绑定数据, 进行下一次操作, 因为绑定数据是每一次执行的时候都会解析并保存的
			// 而第二次以后执行的, 都会再次解析并保存, 数据结构是slice, 故会累积起来
			p.ClearBindValues()
			_ = p.Table(ibinder.GetBindOrigin()).Offset(page * limit).Scan(pointer)
			result = p.GetIBinder().GetBindResultSlice()
		}
	case ObjectStructSlice, ObjectMapSlice, ObjectMapSliceT:
		var ibinder = p.GetIBinder()
		var result = ibinder.GetBindResultSlice()
		for result.Interface() != nil {
			if err = callback(); err != nil {
				break
			}
			page++
			// 清空结果
			result.Set(result.Slice(0, 0))
			// 清理绑定数据, 进行下一次操作, 因为绑定数据是每一次执行的时候都会解析并保存的
			// 而第二次以后执行的, 都会再次解析并保存, 数据结构是slice, 故会累积起来
			p.ClearBindValues()
			_ = p.Table(ibinder.GetBindOrigin()).Offset(page * limit).Scan(pointer)
			result = p.GetIBinder().GetBindResultSlice()
		}
	}
	return
}

// Loop : 同chunk, 不过, 这个是循环的取前limit条数据, 为什么是循环取这些数据呢
// 因为, 我们考虑到一种情况, 那就是where条件如果刚好是要修改的值,
// 那么最后的修改结果因为offset的原因, 只会修改一半, 比如:
// DB().Where("age", 18) ===> DB().Data(gorose.Data{"age":19}).Where().Update()
func (p *Orm) Loop(limit int, callback func([]Data) error) (err error) {
	var page = 0
	var tabname = p.GetISession().GetIBinder().GetBindName()
	prefix := p.GetISession().GetIBinder().GetBindPrefix()
	tabname2 := strings.TrimPrefix(tabname, prefix)
	// 先执行一条看看是否报错, 同时设置指定的limit
	result, err := p.Table(tabname2).Limit(limit).Find()
	if err != nil {
		return
	}
	for len(result) > 0 {
		if err = callback(result); err != nil {
			break
		}
		page++
		// 同chunk
		p.ClearBindValues()
		result, _ = p.Find()
	}
	return
}

// Paginate 自动分页
// @param limit 每页展示数量
// @param current_page 当前第几页, 从1开始
// 以下是laravel的Paginate返回示例
//{
//	"total": 50,
//	"per_page": 15,
//	"current_page": 1,
//	"lastPage": 4,
//	"first_page_url": "http://laravel.app?page=1",
//	"lastPage_url": "http://laravel.app?page=4",
//	"nextPage_url": "http://laravel.app?page=2",
//	"prevPage_url": null,
//	"path": "http://laravel.app",
//	"from": 1,
//	"to": 15,
//	"data":[
//		{
//		// Result Object
//		},
//		{
//		// Result Object
//		}
//	]
//}
func (p *Orm) Paginate(page ...int) (res Data, err error) {
	if len(page) > 0 {
		p.Page(page[0])
	}
	var limit = p.GetLimit()
	if limit == 0 {
		limit = 15
	}
	var offset = p.GetOffset()
	var currentPage = int(math.Ceil(float64(offset+1) / float64(limit)))
	//dba.ResetUnion()
	// 获取结果
	resData, err := p.Find()
	if err != nil {
		return
	}
	// 统计总量
	p.offset = 0
	count, err := p.Count()
	var lastPage = int(math.Ceil(float64(count) / float64(limit)))
	var nextPage = currentPage + 1
	var prevPage = currentPage - 1
	res = Data{
		"total":          count,
		"per_page":       limit,
		"current_page":   currentPage,
		"last_page":      lastPage,
		"first_page_url": 1,
		"last_page_url":  lastPage,
		"next_page_url":  If(nextPage > lastPage, nil, nextPage),
		"prev_page_url":  If(prevPage < 1, nil, prevPage),
		//"data":           dba.GetIBinder().GetBindResultSlice().Interface(),
		"data": resData,
	}

	return
}

// _unionBuild : build union select real
func (p *Orm) unionBuild(union, field string) (interface{}, error) {
	fields := union + "(" + field + ") as " + union
	p.fields = []string{fields}

	res, err := p.First()
	if r, ok := res[union]; ok {
		return r, err
	}
	return 0, err
}

// ValueBak ...
func (p *Orm) ValueBak(field string,pointer ...interface{}) (v interface{}, err error) {
	p.Limit(1)
	err = p.Scan(pointer)
	if err != nil {
		return
	}
	var binder = p.GetISession().GetIBinder()
	switch binder.GetBindType() {
	case ObjectMap, ObjectMapSlice, ObjectMapSliceT, ObjectMapT:
		v = reflect.ValueOf(binder.GetBindResult()).MapIndex(reflect.ValueOf(field)).Interface()
	case ObjectStruct, ObjectStructSlice:
		bindResult := reflect.Indirect(reflect.ValueOf(binder.GetBindResult()))
		v = p.valueFromStruct(bindResult, field)
	case ObjectString:
		res := p.GetISession().GetBindAll()
		if len(res) > 0 {
			v = res[0][field]
		}
	}
	return
}
func (p *Orm) valueFromStruct(bindResult reflect.Value, field string) (v interface{}) {
	ostype := bindResult.Type()
	for i := 0; i < ostype.NumField(); i++ {
		tag := ostype.Field(i).Tag.Get(TagName)
		if tag == field || ostype.Field(i).Name == field {
			v = bindResult.FieldByName(ostype.Field(i).Name).Interface()
		}
	}
	return
}

// PluckBak ...
func (p *Orm) PluckBak(pointer interface{},field string, fieldKey ...string) (v interface{}, err error) {
	var binder = p.GetISession().GetIBinder()
	var resMap = make(map[interface{}]interface{}, 0)
	var resSlice = make([]interface{}, 0)

	err = p.Scan(pointer)
	if err != nil {
		return
	}

	switch binder.GetBindType() {
	case ObjectMap, ObjectMapT, ObjectStruct: // row
		var key, val interface{}
		if len(fieldKey) > 0 {
			key, err = p.Value(fieldKey[0])
			if err != nil {
				return
			}
			val, err = p.Value(field)
			if err != nil {
				return
			}
			resMap[key] = val
		} else {
			v, err = p.Value(field)
			if err != nil {
				return
			}
		}
	case ObjectMapSlice, ObjectMapSliceT:
		for _, item := range t.New(binder.GetBindResultSlice().Interface()).Slice() {
			val := item.MapInterfaceT()
			if len(fieldKey) > 0 {
				resMap[val[fieldKey[0]].Interface()] = val[field].Interface()
			} else {
				resSlice = append(resSlice, val[field].Interface())
			}
		}
	case ObjectStructSlice: // rows
		var brs = binder.GetBindResultSlice()
		for i := 0; i < brs.Len(); i++ {
			val := reflect.Indirect(brs.Index(i))
			if len(fieldKey) > 0 {
				mapkey := p.valueFromStruct(val, fieldKey[0])
				mapVal := p.valueFromStruct(val, field)
				resMap[mapkey] = mapVal
			} else {
				resSlice = append(resSlice, p.valueFromStruct(val, field))
			}
		}
	case ObjectString:
		res := p.GetISession().GetBindAll()
		if len(res) > 0 {
			for _, val := range res {
				if len(fieldKey) > 0 {
					resMap[val[fieldKey[0]]] = val[field]
				} else {
					resSlice = append(resSlice, val[field])
				}
			}
		}
	}
	if len(fieldKey) > 0 {
		v = resMap
	} else {
		v = resSlice
	}
	return
}
